package net.cathackers.devel.scmp.auth.backends;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.ResultSet;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import javax.management.timer.Timer;

import net.cathackers.devel.scmp.auth.exceptions.AuthenticationException;
import net.cathackers.devel.scmp.auth.exceptions.LockedUserException;
import net.cathackers.devel.scmp.auth.exceptions.UserNotFoundException;
import net.cathackers.devel.scmp.auth.impl.UniqueNumberGenerator;
import net.cathackers.devel.scmp.auth.interfaces.AuthToken;
import net.cathackers.devel.scmp.auth.interfaces.AuthenticationCredentials;
import net.cathackers.devel.scmp.auth.interfaces.AuthenticationInformation;
import net.cathackers.devel.scmp.auth.interfaces.Authenticator;
import net.cathackers.devel.scmp.auth.interfaces.UserID;
import net.cathackers.devel.scmp.exceptions.*;
import net.cathackers.devel.scmp.chat.backends.MySQLPoolBackend;
import net.cathackers.devel.scmp.config.impl.AuthenticatorDBConfig;

public class MySQLAuthenticatorBackend extends MySQLPoolBackend implements
	Authenticator {

    // private Cache<AuthToken, AuthenticationCredentials> credentialsCache_;
    private Map<AuthToken, UserID> tokensCache_;

    static final String CONNECTION_POOL_NAME = "scmpAuthentication";
    static final String CONNECTION_POOL_URI = "jdbc:apache:commons:dbcp:"
	    + CONNECTION_POOL_NAME;

    private String authenticateStmt_;
    private String checkForUserStmt_;
    private String lockStmt_;
    private String updateStmt_;
    private UniqueNumberGenerator randomGenerator_;
    private AuthenticatorDBConfig config_;

    public MySQLAuthenticatorBackend(AuthenticatorDBConfig conf) {
	// credentialsCache_ = new HashCache<AuthToken,
	// AuthenticationCredentials>();
	super(conf, CONNECTION_POOL_NAME, CONNECTION_POOL_URI);
	this.config_ = conf;
	tokensCache_ = Collections
		.synchronizedMap(new HashMap<AuthToken, UserID>());
	randomGenerator_ = new UniqueNumberGenerator();

	checkForUserStmt_ = "SELECT `" + config_.getUsernameFieldName()
		+ "`, `" + config_.getIsBlockedFieldName() + "`, `"
		+ config_.getLastLoginFieldName() + "` FROM "
		+ config_.getTableName() + " WHERE `"
		+ config_.getUsernameFieldName() + "` = ?";

	authenticateStmt_ = "SELECT `" + config_.getUsernameFieldName()
		+ "`, `" + config_.getIsBlockedFieldName() + "`, `"
		+ config_.getLastLoginFieldName() + "` FROM "
		+ config_.getTableName() + " WHERE `"
		+ config_.getUsernameFieldName() + "` = ?  AND `"
		+ config_.getPasswordFieldName() + "` = ?";

	lockStmt_ = "UPDATE `" + conf_.getTableName() + "` SET `"
		+ config_.getIsBlockedFieldName() + "` = ? WHERE `"
		+ config_.getUsernameFieldName() + "` = ?";

	updateStmt_ = "UPDATE `" + conf_.getTableName() + "` SET `"
		+ config_.getPasswordFieldName() + "` = ? WHERE `"
		+ config_.getUsernameFieldName() + "` = ?";

    }

    /**
     * check if the backend if alive.
     * 
     * @return true if alive
     * @throws BackendException 
     */
    @Override
    public boolean isAlive() throws BackendException {
	try {
	    return getConnection().isValid(100);

	} catch (SQLException e) {
	    logger_.debug(e.getMessage());
	    throw new BackendException(
		    "Couldn't get the status of the database connection: "
			    + e.getMessage());
	}
    }

    @Override
    public AuthenticationInformation authenticate(
	    AuthenticationCredentials authenticationCredentials)
	    throws LockedUserException, UserNotFoundException, AuthenticationException, BackendConnectException {
	
	SimpleAuthenticationInformation authInformation = null;
	UserID userID = authenticationCredentials.getUserID();
	String username = userID.getUsername();
	byte[] password = authenticationCredentials.getPassword();
	AuthToken authToken;
	Connection conn = null;
	try {
	connect();
	conn = getConnection();
	
	    PreparedStatement stmt = conn.prepareStatement(authenticateStmt_);
	    stmt.setString(1, username);
	    stmt.setString(2, new String(password));
	    ResultSet rs = stmt.executeQuery();

	    if (rs.next()) {

		if (rs.getBoolean(config_.getIsBlockedFieldName())) {
		    throw new LockedUserException();
		}
		authInformation = new SimpleAuthenticationInformation();
		authInformation.setLastLogin(rs.getDate(config_
			.getLastLoginFieldName()));

	    } else {
		throw new UserNotFoundException();
	    }

	    authToken = generateAuthToken(userID);
	    authInformation.setAuthToken(authToken);
	    authInformation.setUserID(userID);

	    logger_.info("user is now authenticated");
            stmt.close();
	} catch (UserNotFoundException ex) {
	    logger_.error("User \"" + userID.getUsername() + "\" not found");
	    throw new UserNotFoundException();

	} catch (LockedUserException ex) {
	    logger_.error("User \"" + userID.getUsername() + "\" is blocked");
	    throw new LockedUserException();

	} catch (BackendException ex) {
	    throw new AuthenticationException();

	} catch (SQLException ex) {
	    logger_.error("Couldn't establish a connection to database : "
		    + conf_.getDBName() + " at : " + conf_.getHostname()
		    + " to table : " + conf_.getTableName());
	    logger_.error(ex.getMessage());
	    throw new BackendConnectException();
	} finally {
	    try {
		this.killConnection(conn);
	    } catch (BackendException e) {
		logger_.error(e.getMessage());
	    } catch (NullPointerException ex) {
	    }
	}

	return (AuthenticationInformation) authInformation;
    }

    private AuthToken generateAuthToken(UserID userID) {
	// TODO read the default time to live for authTokens in memory
	AuthToken authToken = new SimpleAuthToken(this, randomGenerator_
	    .newUniqueLongNumber(), Timer.ONE_MINUTE);
	tokensCache_.put(authToken, userID);
	return authToken;

    }
    
    @Override
    public boolean isAuthTokenValid(AuthToken authToken) {
	return tokensCache_.containsKey(authToken);
    }

    @Override
    public boolean isUsernameExist(String username) throws AuthenticationException, BackendConnectException {
	
	SimpleAuthenticationInformation authInformation = null;
	boolean ret = false;
	Connection conn = null;
	try {
	    connect();
	    conn = getConnection();
	    PreparedStatement stmt = conn.prepareStatement(checkForUserStmt_);
	    stmt.setString(1, username);
	    ResultSet rs = stmt.executeQuery();

	    if (rs.next()) {

		authInformation = new SimpleAuthenticationInformation();
		authInformation.setLastLogin(rs.getDate(config_
			.getLastLoginFieldName()));
		ret = true;
	    }
               stmt.close();
	} catch (BackendException ex) {
	    throw new AuthenticationException();

	} catch (SQLException ex) {
	    logger_.error("Couldn't establish a connection to database : "
		    + conf_.getDBName() + " at : " + conf_.getHostname()
		    + " to table : " + conf_.getTableName());
	    logger_.error(ex.getMessage());
	    throw new BackendConnectException();
	} finally {
	    try {
		this.killConnection(conn);
	    } catch (BackendException e) {
		logger_.error(e.getMessage());
	    } catch (NullPointerException ex) {
	    }
	}

	return ret;
    }

    @Override
    public void logout(AuthToken userAuthToken) {
	tokensCache_.remove(userAuthToken);
    }

    @Override
    public void updateAuthenticationInformation(AuthToken userAuthToken,
	    AuthenticationCredentials newAuthenticationCredentials) throws BackendConnectException, UserNotFoundException {

	updateAuthenticationInformation(tokensCache_.get(userAuthToken),
		newAuthenticationCredentials);
    }

    @Override
    public void lockAccount(AuthenticationCredentials authenticationCredentials) throws BackendConnectException, UserNotFoundException {
	String username = authenticationCredentials.getUserID().getUsername();
	lockUser_(username, true);
    }

    private void lockUser_(String username, boolean toBeLocked) throws UserNotFoundException, BackendConnectException {
	Connection conn = null;
	try {
	    connect();
	    conn = getConnection();
	    PreparedStatement stmt = conn.prepareStatement(lockStmt_);

	    stmt.setBoolean(1, toBeLocked);
	    stmt.setString(2, username);
	    stmt.executeUpdate();
	    if (stmt.getUpdateCount() < 1) {
		throw new UserNotFoundException("User " + username
			+ " was not found!");
	    }
           stmt.close();
	} catch (SQLException ex) {
	    logger_.error("Couldn't establish a connection to database : "
		    + conf_.getDBName() + " at : " + conf_.getHostname()
		    + " to table : " + conf_.getTableName());
	    logger_.error(ex.getMessage());
	    throw new BackendConnectException();
	} catch (BackendException e) {
	    throw new BackendConnectException();
	} finally {
	    try {
		this.killConnection(conn);
	    } catch (BackendException e) {
		logger_.error(e.getMessage());
	    } catch (NullPointerException ex) {
	    }
	}

    }

    @Override
    public void unlockAccount(
	    AuthenticationCredentials authenticationCredentials) throws BackendConnectException, UserNotFoundException {
	String username = authenticationCredentials.getUserID().getUsername();
	lockUser_(username, false);

    }

    @Override
    public void updateAuthenticationInformation(UserID userID,
	    AuthenticationCredentials newAuthenticationCredentials) throws UserNotFoundException, BackendConnectException {
	if (userID == null || newAuthenticationCredentials == null) {
	    logger_.error("userID or newAuthenticationCredentials is null");
	    throw new UserNotFoundException(
		    "Couldn't update user password for this user ");
	}
	
	String username = userID.getUsername();
	byte[] password = newAuthenticationCredentials.getPassword();
	Connection conn = null;
	try {
	    connect();
	    conn = getConnection();
	    PreparedStatement stmt = conn.prepareStatement(updateStmt_);
	    stmt.setString(1, new String(password));
	    stmt.setString(2, username);
	    stmt.executeUpdate();
	    if (stmt.getUpdateCount() < 1) {
		throw new UserNotFoundException(
			"Couldn't update user password for user " + username);
	    }
            stmt.close();
	} catch (SQLException ex) {
	    logger_.error("Couldn't establish a connection to database : "
		    + conf_.getDBName() + " at : " + conf_.getHostname()
		    + " to table : " + conf_.getTableName());
	    logger_.error(ex.getMessage());
	    throw new BackendConnectException();
	} catch (BackendException e) {
	    
	} finally {
	    try {
		this.killConnection(conn);
	    } catch (BackendException e) {
		logger_.error(e.getMessage());
	    } catch (NullPointerException ex) {
	    }
	}
    }
}